import UIKit

class DWSplitDownloader: Operation {
    
    private lazy var operationSemap: DispatchSemaphore = {
        let semap = DispatchSemaphore(value: 0)
        semap.signal()
        return semap
    }()
    
    private (set) var error: DWSplitDownloadError?
    private var taskFinished: Bool = true {
        willSet {
            if taskFinished != newValue {
                willChangeValue(forKey: "isFinished")
            }
        }
        didSet {
            if taskFinished != oldValue {
                didChangeValue(forKey: "isFinished")
            }
        }
    }
    private var taskExecuting: Bool = false {
        willSet {
            if taskExecuting != newValue {
                willChangeValue(forKey: "isExecuting")
            }
        }
        didSet {
            if taskExecuting != oldValue {
                didChangeValue(forKey: "isExecuting")
            }
        }
    }
    
    override var isFinished: Bool {
        return taskFinished
    }
    
    override var isExecuting: Bool {
        return taskExecuting
    }
    
    override var isAsynchronous: Bool {
        return true
    }
    
    var progressBlock: ((Int) -> ())?
    
    var completion: ((Result<(String), DWSplitDownloadError>) -> ())?
        
    var folderPath : String = ""
    
    var splitPath : String = ""
    
    var fileStr : String = ""
    
    private var operationList : [DWSplitDownloadOperation] = []
    
    var splitDownloadModel : DWSplitDownloadModel = DWSplitDownloadModel.init()
    
    lazy var queue: OperationQueue = {
        let _queue : OperationQueue = OperationQueue()
        _queue.maxConcurrentOperationCount = 9
        return _queue
    }()
    
    init(_ _fileStr : String,_ _folderPath : String,_ _splitPath : String, progressBlock: ((Int) -> ())? = nil, completion: ((Result<(String), DWSplitDownloadError>) -> ())? = nil) {
        super.init()
        self.fileStr = _fileStr
        let fileMd5 = _fileStr.dw_down_md5()
        self.folderPath = _folderPath + fileMd5
        self.splitPath = _splitPath + fileMd5
        self.progressBlock = progressBlock
        self.completion = completion
        if !FileManager.default.fileExists(atPath: _folderPath) {
            try? FileManager.default.createDirectory(atPath: _folderPath, withIntermediateDirectories: true)
        }
        if !FileManager.default.fileExists(atPath: _splitPath) {
            try? FileManager.default.createDirectory(atPath: _splitPath, withIntermediateDirectories: true)
        }
    }
    
    func fileTotalBytes(_ url: String) -> Int64 {
        var totalBytes : Int64 = 0
        if let sizeData = try? URL(fileURLWithPath: url).dw_down_extendedAttribute(forName: "totalBytes") {
            (sizeData as NSData).getBytes(&totalBytes, length: sizeData.count)
        }
        return totalBytes
    }
    
    @discardableResult
    func startTask() -> Bool {
        guard let fileUrl = URL.init(string: self.fileStr) else { return false }
        if FileManager.default.fileExists(atPath: self.splitPath + "/info") == true, let data = try? Data.init(contentsOf: URL.init(fileURLWithPath: self.splitPath + "/info")), let dic = self.toDictionary(data: data), let model = transToModel(dic: dic) {
            splitDownloadModel = model
            splitTask()
        }else {
            let infoOpera = DWSplitInfoOperation.init(url: fileUrl) { [weak self](total) in
                guard let strongSelf = self else { return }
                if total <= 0 {
                    try? FileManager.default.removeItem(atPath: strongSelf.splitPath)
                    strongSelf.complete(.notHTTPURLResponse)
                    return
                }
                /// 最小的分块
                let minSplitBytes : Int64 = 1024 * 1024 * 8
                /// 最大的分块
                let maxSplitBytes : Int64 = 1024 * 1024 * 64
                
                var splitBytes : Int64 = minSplitBytes
                let threadCount : Int64 = Int64(strongSelf.queue.maxConcurrentOperationCount)
                
                if total >= maxSplitBytes * threadCount {
                    splitBytes = maxSplitBytes
                }else if total <= minSplitBytes * threadCount {
                    splitBytes = minSplitBytes
                }else {
                    splitBytes = (total / threadCount) + 2
                }
                strongSelf.splitDownloadModel.splitCount = (total / splitBytes) + ((total % splitBytes) > 0 ? 1 : 0)
                strongSelf.splitDownloadModel.totalCount = total
                strongSelf.splitDownloadModel.downloadCount = 0
                strongSelf.splitDownloadModel.originalUrl = strongSelf.fileStr
                strongSelf.splitDownloadModel.path = strongSelf.folderPath
                strongSelf.splitDownloadModel.splitPath = strongSelf.splitPath
                for i in 0..<strongSelf.splitDownloadModel.splitCount {
                    let item = DWSplitDownloadItemModel.init()
                    item.index = i
                    item.path = strongSelf.splitPath + "/_\(item.index)"
                    item.beginBytes = Int64(splitBytes * i)
                    if i == strongSelf.splitDownloadModel.splitCount - 1 {
                        item.endBytes = Int64(min(total, splitBytes * (item.index + 1)))
                    }else {
                        item.endBytes = Int64(min(total, splitBytes * (item.index + 1))) - 1
                    }
                    strongSelf.splitDownloadModel.itemList.append(item)
                }
                if !FileManager.default.fileExists(atPath: strongSelf.splitPath) {
                    try? FileManager.default.createDirectory(atPath: strongSelf.splitPath, withIntermediateDirectories: true)
                }
                strongSelf.saveModelToFile()
                strongSelf.splitTask()
            }
            infoOpera.start()
        }
        return true
    }
    
    func transToModel(dic : [String : Any]) -> DWSplitDownloadModel? {
        let model : DWSplitDownloadModel = DWSplitDownloadModel.init()
        guard let totalCount = dic["totalCount"] as? Int64 else { return nil }
        model.totalCount = totalCount
        guard let downloadCount = dic["downloadCount"] as? Int64 else { return nil }
        model.downloadCount = downloadCount
        guard let originalUrl = dic["originalUrl"] as? String else { return nil }
        model.originalUrl = originalUrl
        guard let splitCount = dic["splitCount"] as? Int64 else { return nil }
        model.splitCount = splitCount
        model.path = self.folderPath
        model.splitPath = self.splitPath
        guard let itemList = dic["itemList"] as? [[String : Any]] else { return nil }
        for item in itemList {
            let itemModel : DWSplitDownloadItemModel = DWSplitDownloadItemModel.init()
            guard let endBytes = item["endBytes"] as? Int64 else { return nil }
            itemModel.endBytes = endBytes
            guard let beginBytes = item["beginBytes"] as? Int64 else { return nil }
            itemModel.beginBytes = beginBytes
            guard let index = item["index"] as? Int64 else { return nil }
            itemModel.index = index
            itemModel.path = self.splitPath + "/_\(index)"
            model.itemList.append(itemModel)
        }
        return model
    }
    
    private func toDictionary(data : Data) -> [String : Any]? {
        if let dic = try? JSONSerialization.jsonObject(with: data,
                           options: .mutableContainers) as? [String : Any] {
            return dic
        }
        return nil
    }
    
    private func saveModelToFile() {
        let dic = splitDownloadModel.saveToDic()
        progressBlock?(Int(splitDownloadModel.downloadCount * 100000 / splitDownloadModel.totalCount))
        if JSONSerialization.isValidJSONObject(dic) == false {
            return
        }
        guard let data = try? JSONSerialization.data(withJSONObject: dic, options: []) else { return }
        do {
            try data.write(to: URL.init(fileURLWithPath: self.splitDownloadModel.splitPath + "/info"))
        } catch {}
    }
    
    private func splitTask() {
        guard let fileUrl = URL.init(string: self.fileStr) else { return }
        var taskCount : Int = splitDownloadModel.itemList.count
        for item in splitDownloadModel.itemList {
            let operation = DWSplitDownloadOperation.init(url: fileUrl, path: item.path, currentBytes: Int64(item.beginBytes), endBytes: Int64(item.endBytes))
            operation.progressBlock = {[weak self](downloadBytes, bytes)in
                guard let strongSelf = self else { return }
                strongSelf.splitDownloadModel.downloadCount += bytes
                strongSelf.saveModelToFile()
            }
            operation.completionBlock = {[weak self]in
                guard let strongSelf = self else { return }
                if let error = operation.error {
                    strongSelf.complete(.download(error))
                    strongSelf.cancel()
                }else {
                    taskCount -= 1
                    if taskCount == 0 {
                        strongSelf.splitDownloadModel.downloadCount = strongSelf.splitDownloadModel.totalCount
                        strongSelf.combinationFile()
                    }
                    strongSelf.saveModelToFile()
                }
            }
            operationList.append(operation)
            queue.addOperation(operation)
        }
    }
    
    private func combinationFile(){
        let sortItemList = splitDownloadModel.itemList.sorted { m1, m2 in
            return m1.index < m2.index
        }
        var damageData : Bool = false
        var outStream = OutputStream.init(toFileAtPath: self.splitDownloadModel.path, append: true)
        outStream?.open()
        for item in sortItemList {
            if let data = try? Data.init(contentsOf: URL.init(fileURLWithPath: item.path)) {
                if damageData == false {
                    outStream?.write(Array(data), maxLength: data.count)
                }
            }else {
                outStream?.close()
                outStream = nil
                damageData = true
                do {
                    try FileManager.default.removeItem(atPath: item.path)
                } catch { }
            }
        }
        outStream?.close()
        outStream = nil
        if damageData == false {
            do {
                try FileManager.default.removeItem(atPath: self.splitDownloadModel.splitPath)
            } catch {
                complete(.fileCompound)
            }
        }
        complete()
    }
    
}

extension DWSplitDownloader {
    override func start() {
        autoreleasepool {
            if isCancelled || FileManager.default.fileExists(atPath: self.folderPath) {
                if FileManager.default.fileExists(atPath: self.folderPath) == true {
                    try? FileManager.default.removeItem(atPath: self.splitPath)
                }
                taskFinished = true
                taskExecuting = false
            }else {
                taskFinished = false
                taskExecuting = true
                startTask()
            }
        }
    }
    
    override func cancel() {
        if (isExecuting) {
            for operation in self.operationList {
                operation.cancel()
            }
        }
        super.cancel()
    }
    
}
private extension DWSplitDownloader {
    
    func complete(_ error: DWSplitDownloadError? = nil) {
        self.error = error
        taskFinished = true
        taskExecuting = false
    }
    
}
